// post.js - mirror to render.js

// single instance is for single pre
// first phase: args null
var Post = function( params ) {

	//-console.log( 'Post: '+el );

	var match_arr, pre = '';

	var post = this;
	post.args = params.args || {};
	post.itemArr = [];				// array of columns
	post.state = {}
	post.state.el = params.el,
	post.state.pre = (params.pre!==undefined) ? params.pre : Post.pre(post.state.el);
	post.state.idx = (params.idx!==undefined) ? params.idx : Post.idx(post.state.el);
	post.state.col = (params.col!==undefined) ? params.col : Post.col(post.state.el);
	post.state.update  = (params.update!==undefined) ? params.update : Post.picAttribute( post.state.pre, post.state.idx, post.state.col, 'update' )  || null;
	post.state.dml     = (params.dml!==undefined) ? params.dml : Post.picAttribute( post.state.pre, post.state.idx, post.state.col, 'dml' )  || null;
	post.state.request = (params.request!==undefined) ? params.request : Post.picAttribute( post.state.pre, ((post.state.update=='form')?null:post.state.idx), ((post.state.update=='col')?'col':null), 'request' ) || post.state.update;
	post.state.embold  = (params.embold!==undefined) ? params.embold : Post.picAttribute( post.state.pre, ((post.state.update=='form')?null:post.state.idx), ((post.state.update=='col')?'col':null), 'embold' )  || null;

	post.state.scope = params.scope;
	if( !post.state.scope && post.state.col ) scope = 'col';
	if( !post.state.scope && post.state.el.hasAttribute('tup') ) scope = 'row';
	if( !post.state.scope && post.state.el.hasAttribute('rel') ) scope = 'form';

	post.state.renders = params.renders || {};
	if( !params.renders && params.render ) post.state.renders[params.render.pre()] = params.render;

	// if we're not entrant, move up until find rel or tup
	if( !post.state.scope ) throw( "no context from which to create New Post" );

	// case one: form parent - get all pre's as set in rel
	// todo: multiple pre's
	if( post.state.scope == 'form' ) {

		// pre
		post.relMeta( post.state.pre );

		// delete - all we need is pkval
		if( dml == 'delete' ) {
			Post.querySelectorAll("[tup^='"+post.state.pre+":']").forEach( function(el) {
				var idx = el.getAttribute('tup').split(':')[1];
				var pkval = el.getAttribute(post.state.pre+':'+idx+':pkval');
				if( pkval ) post.args[post.state.pre]['rows'][idx] = {pkval:pkval, dml:'delete'};
			} );
		}

		// otherwise, find all elements, backfill tup as necessary
		else	Post.querySelectorAll("[name^='"+pre+":']").forEach( post.attMeta(el) );
	}

	// case two: row
	// todo: more than one idx in tup
	else if( post.state.scope=='row' ) {

		// pre, tup
		post.relMeta( post.state.pre );
		post.tupMeta( post.state.pre, post.state.idx );
	
		// delete - set meta
		if( post.state.dml == 'delete' ) post.args[post.state.pre].rows[post.state.idx].dml = 'delete';
		
		// elements
		else	Post.querySelectorAll("[name^='"+pre+":"+idx+":']").forEach( post.attMeta(el) );
	}

	// case three: entry, pre determined by item.name
	else if( post.state.scope=='col' ) {

		// get col meta data
		post.attMeta( post.state.el );
	}
}

// accessor functions
// note: only using attribute for name due to form.input names
Post.pre = function(el) {
	var name = (typeof el == 'string') ? el : ((el.hasAttribute('name')) ? el.getAttribute('name') : undefined );
	// name is ':' delimited - legal name we assume
	if( (name && name!='') && name.indexOf(':')>0 ) return name.split(':')[0];
	if( typeof el == 'string' ) return undefined;
	// no name so far - let's look upward to tup
	name = el.ancestorGetAttribute('tup',true);
	if( (name && name!='') && name.indexOf(':')>0 ) return name.split(':')[0];
	// no name so far - let's look upward to tup
	name = el.ancestorGetAttribute('rel',true);
	if( name && name!='' ) return name;
	// cannot get proper name
	throw( 'unable to get pre for el' );
}
// arg can be element or pre string
Post.pntPre = function(el) {
	var pre = (el instanceof HTMLElement) ? Post.pre(el) : el;
	var preArr = pre.split('.');
	preArr.pop();
	pre = preArr.join('.');
	preArr = pre.split('-');
	preArr.pop();
	return preArr.join('-');
}
Post.idx = function(el) {
	var name = el.name;
	if( name && name.indexOf(':')>0 ) return name.split(':')[1];
	var tup = el.ancestorGetAttribute('tup',true);
	if( tup ) return tup.split(':')[1];
	return null;
}
Post.pntIdx = function(el) {
	var pre = (el instanceof HTMLElement) ? Post.pre(el) : el;
	var preArr = pre.split('.');
	preArr.pop();
	pre = preArr.join('.');
	preArr = pre.split('-');
	return preArr.pop();
}
Post.col = function(el) {
	var name = el.name;
	if( !name ) return null;
	if( name.indexOf(':')>0 ) return name.split(':')[2];
	return el.name;
}
Post.colUpdateable = function(el) {
	if( el.getAttribute('updateable')!='false' ) return false;
	if( el instanceof HTMLInputElement ) return( !['hidden','submit'].has(el.type) );
	if( el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement ) return true;
	return( el.isContentEditable === true );
}
Post.pkval = function( pre, idx ) {
	return Post.queryAttribute(pre+":"+idx+":pkval");
}
Post.picAttribute = function( pre, idx, col, attr ) {
	// overload
	if( pre instanceof HTMLElement ) {
		attr = idx;
		col = Post.col(pre);
		idx = Post.idx(pre);
		pre = Post.pre(pre);
	}
	var tmp, result;
	if( col!==null && (tmp=Post.querySelector("[name='"+pre+":"+idx+":"+col+"']")) && tmp.hasAttribute(attr) )
		result = tmp.getAttribute(attr);
	else if( idx!==null && (tmp=Post.querySelector("["+pre+":"+idx+":"+attr+"]")) )
		result = tmp.getAttribute(pre+":"+idx+":"+attr);
	else if( pre!==null && (tmp=Post.querySelector("["+pre+":"+attr+"]")) ) 
		result = tmp.getAttribute(pre+":"+attr);
	else return null;
	return (result=='parent') ? Post.picAttribute( Post.pntPre(pre), Post.pntIdx(pre), null, attr ) : result;
}
Post.querySelector = function( source, pattern ) {
	if( pattern==undefined ) { pattern=source; source=document; }
	var fixedPattern = pattern.replace( /\[.*?\]/g, function(str) { return str.replace(/[:.]/g,"\\$&"); } );
	return source.querySelector(fixedPattern);
}
Post.queryAttribute = function( source, attr ) {
	var el = Post.querySelector(source,"["+attr+"]");
	return (el) ? el.getAttribute(attr) : null;
}
// note: returns an array(!)
Post.querySelectorAll = function( source, pattern ) {
	if( pattern==undefined ) { pattern=source; source=document; }
	var fixedPattern = pattern.replace( /\[.*?\]/g, function(str) { return str.replace(/[:.]/g,"\\$&"); } );
	return [].slice.call(source.querySelectorAll(fixedPattern));
}

// rel meta
Post.prototype.relMeta = function( pre ) {

	var post = this;

	// add to args if not there
	if( post.args[pre]==undefined ) post.args[pre] = { 'pre':pre, 'rows':{} };

	// extract rel meta attributes
	state.dd['~'].post.rel.forEach( function(prop) {
		var a = Post.querySelector("["+pre+':'+prop+"]");
		if( a ) post.args[pre][prop] = a.getAttribute(pre+':'+prop);
	} );
}

// pull tuple meta
Post.prototype.tupMeta = function( pre, idx ) {

	var post = this;

	// add to args if not there
	if( post.args[pre] == undefined ) post.relMeta(pre);
	if( post.args[pre].rows[idx]==undefined ) post.args[pre].rows[idx] = { 'idx':idx, 'cols':{} };

	// extract row meta attributes
	state.dd['~'].post.tup.forEach( function(prop) {
		var el = Post.querySelector("["+pre+':'+idx+':'+prop+"]");
		if( el ) post.args[pre].rows[idx][prop] = el.getAttribute(pre+':'+idx+':'+prop);
	} );
}

// col meta
Post.prototype.attMeta = function( pre, idx, col ) {

	var post = this;

	var el = (pre instanceof HTMLElement) ? pre : Post.colDom(pre,idx,col);
	if( !el ) throw( 'unable to find element' );
	if( pre instanceof HTMLElement ) {
		pre = Post.pre(el);
		idx = Post.idx(el);
		col = Post.col(el);
	}

	//-console.log( 'attMeta: '+pre+', '+idx+', '+col+', '+JSON.stringify(post.args) );
	if( post.args[pre] == undefined ) post.relMeta(pre);
	if( post.args[pre].rows[idx] == undefined ) post.tupMeta(pre,idx);
	if( post.args[pre].rows[idx].cols[col]==undefined ) post.args[pre].rows[idx].cols[col] = { 'col':col };

	// prefix to naming
	var prefix = pre+':'+idx+':';

	// extract column meta attributes - only in element!!
	//~for( var i=0; i<state.dd['~'].post.att.length; i++ ) {
	state.dd['~'].post.att.forEach( function(prop) {
		if( post.args[pre].rows[idx][prop]==undefined ) {
			if( el.hasAttribute(prop) )  post.args[pre].rows[idx].cols[col][prop] = el.getAttribute(prop);
		}
	} );

	// value: property, attribute, innerHTML
	post.args[pre].rows[idx].cols[col]['value'] = Post.colValue(el);
	//-console.log( pre+':'+idx+':'+col+' value = '+Post.colValue(el)+', '+el.name+'='+el.value );
	post.args[pre].rows[idx].cols[col]['dataValue'] = Post.colDataValue(el);

	// add to arr
	post.itemArr.push(el);

	// add dependent items
	if( el.hasAttribute('depon') ) post.attMeta( pre, idx, el.getAttribute('depon') );
};

// sibling - item sharing same record as el
Post.querySiblingSelector = function( el, arg ) {
	return Post.querySelector( "[name^='"+Post.pre(el)+':'+Post.idx(el)+":'] "+arg );
}
Post.querySiblingSelectorAll = function( el, arg ) {
	return Post.querySelectorAll( "[name^='"+Post.pre(el)+':'+Post.idx(el)+":'] "+arg );
}
Post.colSibling = function( el, name ) {
	return Post.querySelector( "[name='"+Post.pre(el)+':'+Post.idx(el)+':'+name+"']" );
}
Post.colSiblingValue = function( el, name ) {
	var sib = Post.colSibling( el, name );
	if( !sib ) console.log( 'colSiblingValue for '+el.name+', '+name+' = undefined' );
	return ( sib ) ? Post.colValue( sib ) : undefined;
}
Post.colSiblingSetValue = function( el, name, value ) {
	var sib = Post.colSibling( el, name );
	if( sib ) Post.colSetValue( sib, value );
}
// pulls value from
Post.checkboxFalse  = { 1: '0', t: 'f', y: 'n', Y: 'N' };
Post.colDom = function( pre, idx, col ) {
	var els = document.getElementsByName(pre+':'+idx+':'+col);
	//?if( els.length != 1 ) throw( 'unable to pull column' );
	if( els.length == 0 ) return null;
	return els[0];
}
Post.colValue = function( pre, idx, col ) {

	// might have been passed pre/idx/col
	var el;
	if( pre instanceof HTMLElement )	el = pre;
	else					el = Post.colDom( pre, idx, col );

	// not found - not throw error
	if( !el ) return null;

	// if checkbox
	if( el instanceof HTMLInputElement && el.type=='checkbox' ) {
		if( el.checked ) return el.value;
		return ( Post.checkboxFalse[el.value]==undefined ) ? '0' :  Post.checkboxFalse[el.value];
	}

	// todo: radio
	if( el instanceof HTMLInputElement && el.type=='radio' ) {
	}

	// select, input and textarea
	if( el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ) return el.value;

	// non-entry
	if( typeof el['value'] != 'undefined' ) return( el.value );
	if( el.hasAttribute('value') ) return el.getAttribute('value');
	return el.textContent;
}
Post.colSetValue = function( pre, idx, col, value ) {

	// might have been passed pre/idx/col
	var el;
	if( pre instanceof HTMLElement ) {
		el = pre;
		value = idx;
	}
	else	el = Post.colDom(pre,idx,col);

	//-console.log( 'colSetValue( '+el+', '+value+' )' );

	// if checkbox
	if( el instanceof HTMLInputElement && el.type=='checkbox' ) {
		el.checked  = (el.value == value);
		el.defaultChecked  = el.checked;
	}

	// todo: radio
	else if( el instanceof HTMLInputElement && el.type=='radio' ) {
	}

	// has value property
	else if( typeof el['value'] != 'undefined' ) {
		el.value = value;
		//~el.setAttribute('dataValue',value);
	}

	// entry
	else if( el.hasAttribute('value') ) {
		el.setAttribute('value',value);
		//~el.setAttribute('dataValue',value);
		if( el.hasOwnProperty('textContent') ) el.textContent = value;
	}

	// non-entry
	else	el.textContent = value;
}

/*
Post.colCalc = function( el, stack ) {

	var post = this;

	// must exist and have calc attr
	if( !el || !el.hasAttribute('calc') ) return true;

	// build calc for eval
	var calcArr = el.getAttribute('calc').match(/"[^"]*"|[\S]+/g);
	calcArr.forEach( function( str, i ) {
		if( /^\w+$/.test(str) ) calcArr[i] = "Post.colSiblingValue( el,'"+str+"') )";
	} );
	// evaluate
	// todo: if eval fails
	try{ var value = eval( calcArr.join(' ') ); }
	// calc failed
	catch(e) {
		console.log( "eval error: "+el.getAttribute('calc') );
		// try to get default from dd
		if( post instanceof Post && post[Post.pre(el)] &&
			post[Post.pre(el)].dd &&
			post[Post.pre(el)].dd.cols[Post.col(el)] &&
			post[Post.pre(el)].dd.cols[Post.col(el)].default !== undefined ) value = post[Post.pre(el)].dd.cols[Post.col(el)].default;
		// otherwise give 'n/a'
		else	value = 'n/a';
	};
	//-console.log( 'colCalc '+el+', calc='+el.getAttribute('calc')+', arr='+JSON.stringify(calcArr)+', value='+value );
	Post.colSetValue(el,value,stack);
	return true;
}
*/

Post.colDataValue = function( pre, idx, col ) {

	// overload
	var el;
	if( pre instanceof HTMLElement )	el = pre;
	else					Post.colDom( pre, idx, el );

	/* if checkbox
	if( el instanceof HTMLInputElement && el.type=='checkbox' ) {
		if( el.defaultChecked ) return el.value;
		return ( Post.checkboxFalse[el.value]==undefined ) ? '0' :  Post.checkboxFalse[el.value];
	}

	// todo: radio
	if( el instanceof HTMLInputElement && el.type=='radio' ) { }
	*/

	// this should work everywhere
	return el.getAttribute('dataValue') || '';
}

Post.colChanged = function( pre, idx, col ) {

	// overload
	var el;
	if( pre instanceof HTMLElement )	el = pre;
	else					Post.colDom( pre, idx, el );

	// todo: radio
	//-console.log( 'change '+el.name+': "'+Post.colDataValue(el)+'" == "'+Post.colValue(el) );
	return Post.colValue(el) != Post.colDataValue(el);
}

Post.colParam = function( pre, idx, col, param ) {

	// overload
	var el;
	if( pre instanceof HTMLElement )	el = pre;
	else					Post.colDom( pre, idx, el );

	function result( arg ) {
		if( arg == 'true' ) return true;
		if( arg == 'false' ) return false;
		return arg;
	}
	if( el.hasOwnProperty(param) ) return result( el[param] );
	if( el.hasAttribute(param) ) return result( el.getAttribute(param) );
	return undefined;
}

// bold/normal according to whether anything changed
Post.prototype.embold = function( el ) {

	var post = this;
	//-console.log( 'embold = '+post.state.embold );
	if( [null,'false','none'].has(post.state.embold) ) return true;

	// level of embold
	switch( post.state.embold ) {
		case 'form':	el = Post.querySelector("[rel='"+post.state.pre+"']");
				break;
		case 'row':	el = Post.querySelector("[tup='"+post.state.pre+":"+post.state.idx+"']");
				break;
		case 'col':	el = Post.querySelector("[name='"+post.state.pre+":"+post.state.idx+":"+post.state.col+"']");
				break;
		default:	el = null;
	}
	if( !el ) console.log( 'Internal ERROR: incorrect type for embold = '+embold );

	//-console.log( 'Post.embold: '+el );

	var changed = (Post.changeCt(el)>0);
	[].forEach.call( el.getElementsByClassName('embold'), function(el1) {
		//-console.log( 'set embold for '+el1+', changed='+changed );
		el1.style.fontWeight = (changed) ? 'bold' : 'normal';
		el1.style.color = (changed) ? 'darkred' : null;
	} );

	// done
	return true;
}

// children of pre [/ idx]
Post.cols = function( pre, idx, includeNonUpdateable ) {
	var cols = [];
	Post.querySelectorAll("[name^='"+pre+((idx)?":"+idx:'')+":']").forEach( function(el) {
		if( includeNonUpdateable || el.getAttribute('updateable')!='false' ) cols.push(el);
	} );
	return cols;
}

// list number of items that can be updated for row
Post.changeCt = function( el, includeNonUpdateable ) {

	var prefix = '';
	if( el.name && el.name.split(':')[2] ) prefix = el.name;
	else if( el.hasAttribute('tup') ) prefix = el.getAttribute('tup')+':';
	else if( el.hasAttribute('rel') ) prefix = el.getAttribute('rel')+':';
	else throw( 'Post.changeCt - incorrect el: '+el );

	return Post.querySelectorAll("[name^='"+prefix+"']").reduce( function(sum,el1) {

		// nonUpdateable
		if( !includeNonUpdateable && el1.getAttribute('updateable')=='false' ) return sum;

		// if changed
		return ( Post.colChanged(el1) ) ? sum+1 : sum;
	}, 0 );
}

// change as called from INPUT, typically be Render
Post.change = function( params ) {

	// if called as trigger
	if( params instanceof Event ) params = { e:params, el:params.target };

	// mark this as changed so upstream will know - prevent recursive calls
	if( params.e ) params.e.change = true;

	// could be passed event
	if( params instanceof Event ) params = {e:params, el:params.target};

	//-console.log( 'starting Post.change on '+params.el.name );

	// keep self from being called twice
	if( params.e.changeCalled )  return;
	else params.e.changeCalled = true;

	// post to handle local
	params.scope = 'col';
	post = new Post( params );

	// each of change subfuncs
	['helper','lookup','validate','calc','update','embold'].every( function(type) {
		var success = post[type].call(post,params.el);
		//-console.log( "subfunc "+type+" = "+success );
		return success;
		//-return post[type].call(post,params.el) } );
	} );
}

// update as called from dom, typically by user code submit
Post.update = function( params ) {

	//-console.log( 'Post.update' );

	// if called as trigger
	if( params instanceof Event ) params = { e:params, el:params.target };

	// parse args
	var orig_el = params.el || (params.e && params.e.target);

	//-e.stopPropagation(); e.preventDefault();
	//-console.log( 'Post.update: '+orig_el );
	if( !orig_el ) throw( 'stop' );

	// (initial) scope of update
	if( !params.pre ) params.pre = Post.pre(orig_el);
	if( !params.idx ) params.idx = Post.idx(orig_el);
	if( !params.col ) params.col = Post.col(orig_el);
	if( !params.update ) params.update = Post.picAttribute( params.pre, params.idx, params.col, 'update' )  || 'col';
	if( !params.dml ) params.dml = Post.picAttribute( params.pre, params.idx, params.col, 'dml' )  || null;
	if( !params.request ) params.request = Post.picAttribute( params.pre, ((params.update=='form')?null:params.idx), ((params.update=='col')?params.col:null), 'request' ) || params.update;
	if( !params.embold ) params.embold = Post.picAttribute( pre, ((update=='form')?null:idx), ((update=='col')?col:null), 'embold' )  || null;

	// no update
	if( params.update == 'none' || params.dml == 'none' ) return;

	// update is row or from while el is col
	if( params.update != 'col' && Post.isCol(orig_el,true)
	||  params.update == 'col' &&( orig_el.getAttribute('updateable')=='false' || !Post.pkval(pre,idx) ) ) {
		console.log( 'did not update, el='+orig_el+', update='+update );
		return;
	}

	// start with col - unset if not col update
	if( params.update=='col' ) params.el = orig_el;
	else if( params.update=='row' )	params.el = Post.querySelector("[tup='"+params.pre+":"+params.idx+"']");
	else if( params.update=='form' ) params.el = Post.querySelector("[rel='"+params.pre+"']");

	// proceed with update
	var post = new Post( params );
	post.update( el );
}

// update as called from Post.update as as change subfunc
Post.prototype.update = function( el ) {

	post = this;

	//-console.log( 'post.update: update='+post.state.update+', request='+post.state.request );

	// shold we be updating?
	if( post.state.scope != post.state.update ) {
		console.log( 'not updating: scope='+post.state.scope+', update='+post.state.update );
		return true;
	}

	// validate all - will call update Message when done
	if( !post.validate( el ) ) return false;

	return post.callSubChangeFunc.call( post, el, 'update', function() {

		// attempt actual update
		var success = false;
		new Message( 'dml', {'request':post.state.request,'args':post.args}, null, function(msg) {

			var request = msg.result.request;
			//-console.log( 'dml success: request='+request );

			// col - update each col
			if( request == 'col' ) {

				// go through rels
				msg.result.rows.forEachProperty( function(pre) {
				
					// go through tups
					msg.result.rows[pre].forEachProperty( function(idx) {

						// go through cols - set value
						msg.result.rows[pre][idx].forEachProperty( function(col) {
							Post.colSetValue( pre, idx, col, msg.result.rows[pre][idx][col] );
						} );

						// embold for row
						if( Post.picAttribute( pre, idx, null, 'update' ) == 'row' ) post.embold();
					} );
					if( !Post.colUpateable(el) && embold == 'form' ) success = post.embold(el);
					else success = true;
				} );
			}

			// row - replace row
			else if( request == 'row' || request == 'row-extra' ) {

				//-console.log( 'request = row' );

				// each pre in result
				msg.result.rows.forEachProperty( function(pre) {

					// each idx in pre
					msg.result.rows[pre].forEachProperty( function(idx) {

						idx = parseInt(idx);
						var tup = Post.querySelector("[tup='"+pre+":"+idx+"']");
						//-console.log( 'row: '+pre+':'+idx+', tup='+tup );
						// delete
						if( msg.result.rows[pre][idx]==null ) {
							post.state.renders[pre].state.rcds[idx-1] = null;
							tup.parentNode.removeChild( tup );
						}
						// todo: insert row
						else if( !tup ) {
						}
						// update row
						else {
							post.state.renders[pre].rcds()[idx-1] = msg.result.rows[pre][idx];
							post.state.renders[pre].seek( idx );
							var newTup = post.state.renders[pre].rowRender();
							var pnt = tup.parentNode;
							//-var sibling = tup.nextNode;
							//-if( !pnt ) throw( 'no parent node for tup!' );
							//-pnt.removeChild( tup );
							pnt.replaceChild( newTup, tup );
							if( post.state.embold == 'row' ) post.embold();
						}
					} );
					if( Post.colUpdateable(el) && post.state.embold == 'form' ) post.embold(el);
				} );

				// extra row
				if( request == 'row-extra' && post.state.renders[pre] ) post.state.renders[pre].extraRow(true);
			}

			// form - replace form for each pre
			else if( request == 'from' ) msg.result.rows.forEachProperty( function(pre) {

				//-console.log( 'form: '+pre );
				post.state.renders[pre].rows = msg.result.rows[pre];
				post.state.renders[pre].rewind();
				post.state.renders[pre].formRender();
				if( post.state.embold == 'form' ) post.embold();
			} );

			// content
			else if( request == 'content' ) msg.result.rows.forEachProperty( function(pre) {

				//-console.log( 'content: '+pre );
				if( post.state.renders[pre] && post.state.renders[pre].contentRender ) post.state.renders[pre].contentRender();
			} );

			// return success
			success = true;

		}, function(msg) {

			//-console.log( 'dml failure' );
			var err = (msg.result.status=='failure') ? msg.result.message : null;
			if( err ) nonblockAlert( err );
			success = false;
		} );

		return success;
	} );
}

// if func==function or context[func...]==function, return it; otherwise undefined
Post.prototype.callSubChangeFunc = function( pre, idx, col, subChange, origFunc ) {

	// overload
	var el;
	if( pre instanceof HTMLElement ) {
		el = pre;
		subChange = idx;
		origFunc = col;
	}
	else	el = Post.colDom( pre, idx, col );

	var post = this;
	var funcName = Post.picAttribute( Post.pre(el), Post.idx(el), Post.col(el), 'change.'+subChange );
	if( !funcName ) return origFunc( el );

	// search for function in different scopes
	[{context:post,path:'state.renders.'+Post.pre(el)+'.'+funcName},
	 {context:post,path:funcName},
	 {context:Post,path:funcName},
	 {context:window,path:funcName}].every( function(pair) {

		// dotted notation into real object
		base = pair.context;
		pair.path.split('.').every( function(part){ return( base = base[part] ); } );

		// function exists and call before
		if( base && typeof base == 'function' && callWhen ) {
			var result = true;
			base.call( post, el, function(err) {
				if( err === true ) result = origFunc.call(post);
				else result = (err!==false);
			} );
			return result;
		}

		// function exists and call after
		else if( base && typeof base == 'function' && !callWhen ) {
			if( !origFunc.call(post) ) return false;
			var result = true;
			base.call( post, el, function(err) {
				result = ( err !== false );
			} );
			return result;
		}

		// todo: function not exists
		return origFunc.call(post,el);
	} );
	// did not find func, origFunc
};

// return success
Post.prototype.helper = function( el ) {

	//-console.log( 'calling helper' );

	// parse args - if "none", no lookup
	if( [ 'none', 'false' ].has(el.getAttribute('helper')) ) return true;

	// call helper func
	var post = this;
	return post.callSubChangeFunc.call( post, el, 'helper', function() {

		var helper = el.getAttribute('helper');
		if( helper == 'true' ) helper = el.getAttribute('type');
		if( [null,undefined,'none','false'].has(helper) ) return true;

		var val = Post.colValue(el);

		// initial help
		switch( helper ) {

		case 'time':	try{ val = util.fixTime(val); }
				catch(err) { el.focus(); util.nonblockAlert(err); return false; }
				break;
		case 'date':	try{ val = util.fixDate(val); }
				catch(err) { el.focus(); util.nonblockAlert(err); return false; }
				break;
		case 'datetime': try{ val = util.fixDateTime(val); }
				catch(err) { el.focus(); util.nonblockAlert(err); return false; }
				break;
		case 'number':	try{ val = util.fixNumber(val,el.getAttribute('precision')); }
				catch(err) { el.focus(); util.nonblockAlert(err); return false; }
				break;
		case 'default':	return true;
		}

		// done successfully
		Post.colSetValue(el,val);
		return true;
	} );
}

Post.prototype.lookup = function( el ) {

	//-console.log( 'lookup' );

	var post = this;

	// turn off lookup for col - default off
	var lookup = el.getAttribute('lookup');
	if( [ null, 'none', 'false' ].has(lookup) ) return true;

	// see if user func
	return post.callSubChangeFunc.call( post, el, 'lookup', function() {

		// default lookup
		if( lookup == 'true' ) {
			var pre = Post.pre(el);
			var dd = post.args[pre].dd;
			var col = Post.col(el);
			var cattrs = state.dd[dd].cols[col];
			lookup = { fkcol:state.dd[post.args[Post.pre(el)].dd].cols[Post.col(el)].fkcol, match:'exact'};
		}
		else if( typeof lookup == 'string' ) {
			if( lookup[0]=='{' ) {
				try{ lookup = JSON.parse(lookup); }
				catch(e) { lookup = { fkcol:lookup, match:'exact' } };
			}
			else lookup = { fkcol:lookup, match:'exact' }
		}

		//-console.log( 'lookup: '+JSON.stringify(lookup) );

		// foreign key  item
		var fkdom = Post.querySelector("[name='"+Post.pre(el)+':'+Post.idx(el)+':'+lookup.fkcol+"']");
		if( !fkdom ) throw( 'locate cannot find fkcol' );

		// null value
		if( Post.colValue(el) == '' ) {
			// todo: blank all relevant
			Post.colSetValue(fkdom,'');
			return true;
		}

		// validate success
		var success;
		new Message( 'lookup', { el:el.name, lookup:lookup, args:post.args}, null,
			function(msg) {
				// assume one return
				if( msg.result.rows.length == 1 ) {
					var rcd = msg.result.rows[0];
					var prefix = Post.pre(el)+":"+Post.idx(el)+":";
					var col;
					rcd.forEachProperty( function(renderCol) {
						// col same as renderCol
						if( col = Post.querySelector("[name='"+prefix+renderCol+"']") ) Post.colSetValue(col,rcd[renderCol]);
						// col attr same as renderCol
						if( col = Post.querySelector("[name^='"+prefix+"'][col='"+renderCol+"']") ) Post.colSetValue(col,rcd[renderCol]);
					} );
				};
				success = true;
			},
			function(msg) {
				Post.colSetValue(post.state.el,'');
				post.state.el.focus();
				nonblockAlert( msg.result.message );
				success = false;
			} );

		// done with message callbacks
		return success;
	} );
}

Post.prototype.validate = function( el ) {

	//-console.log( 'validate subfunc' );
	var post = this;

	// turn off validation for col - default true
	if( [ 'none', 'false' ].has(el.getAttribute('validate')) ) return true;

	// user func
	return post.callSubChangeFunc.call( post, el, 'validate', function() {
		return post.itemArr.every( function(el1) { return post.colValidate(el1)} );
	} );
}
Post.prototype.calc = function( el ) {

	//-console.log( 'change.calc' );
	var post = this;
	var pre = Post.pre(el);
	var idx = Post.idx(el);
	var col = Post.col(el);

	// find all calc in same idx
	var calcs = Post.querySelectorAll("[name^='"+pre+":"+idx+":'][calc]");
	//-console.log( 'calcs for '+pre+':'+idx+' = '+calcs.length );
	return calcs.every( function(el1) { return post.callSubChangeFunc.call( post, el1, 'calc', function() {

		// pull out each potential column
		var newCalc = '', matchArr, cdrCalc=el1.getAttribute('calc');
		var changeEl = false;
		while( matchArr = cdrCalc.match( /(^|.*?[^\\\w.])(\'|\"|[A-Za-z]\w*)($|[^\w.\(].*)/ ) ) {
			if( matchArr[2] == Post.col(el) ) changeEl = true;
			// quote block
			if( matchArr[2]=='"' || matchArr[2]=="'" ) {
				newCalc += matchArr[1] + matchArr[2];
				//-console.log( 'quote: newCalc = '+newCalc );
				matchArr = (matchArr[2]=='"') ?  matchArr[3].match( /(.*?[^\\]\")(.*)/ ) : matchArr[3].match( /(.*?[^\\]\')(.*)/ );
				//-console.log( 'calc quote: 1 = '+matchArr[1]+', 2 = '+matchArr[2] );
				newCalc += matchArr[1];
				cdrCalc = matchArr[2];
			}
			// variable name
			else {
				//-console.log( 'calc name('+matchArr[2]+'): '+newCalc+matchArr[1]+matchArr[2]+matchArr[3] );
				newCalc += matchArr[1] + "Post.colValue('"+pre+"','"+idx+"','"+matchArr[2]+"')";
				cdrCalc = matchArr[3];
			}
			//-console.log( 'cdrCalc = '+cdrCalc );
			if( newCalc.length > 800 ) throw( 'too long' );
		}
		newCalc += cdrCalc;
		//-console.log( 'changeEl='+changeEl+', calc: '+newCalc );

		// if el's column found, calculate
		if( changeEl ) {
			var val;
			try{ val = eval( newCalc ); }
			catch(e) { util.nonblockAlert( "INTERNAL ERROR: cannot calculate\n"+newCalc ); return false; }
			//-console.log( 'set '+Post.col(el1)+' = '+val );
			Post.colSetValue(el1,val);
			post.attMeta(el1);
			//-console.log( 'val = '+Post.colValue(el1) );
		}

		// todo: ripple down to depon
		// todo: ripple out to other calcs dependent on this or on a depon
		// todo: ripple upward to parent calcs, eg, total price
		
		// success
		return true;
	} ); } );
}
// has name format pre:idx:col and is updateable (by default)
Post.isCol = function( el, nonupdateable ) {
	if( nonupdateable ) return( el.hasAttribute('name') && (el.getAttribute('name').split(':').length==3) );
	return( el.hasAttribute('name') && (el.getAttribute('name').split(':').length==3) && (el.getAttribute('updateable')!='false') );
}

// callable validate
Post.colValidate = function( e ) {
	//-console.log( 'Post.colValidate '+e.target );
	var el = (e instanceof Event) ? e.target : e;
	var post = new Post( {el:el,scope:'col',dml:'none'} );
	post.validate( el );
}

// colValidate as called from this
// nextFunc mandatory
Post.prototype.colValidate = function( el ) {

	//-console.log( "colValidate for "+el.name+', valid='+el.checkValidity() );

	// if already invalid at html5 level
	if( !el.checkValidity() && !el.validity.customError ) {
		console.log( 'invalid: '+el.name+' - ' +el.validationMessage );
		el.focus();
		return false;
	}
	
	var err = null, errArr = [];

	// required
	//=if( Post.colParam(el,'required') ) this.colRequired( el, next );

	// enum
	if( Post.colParam(el,'enum') && ( err = this.colEnum(el) ) ) errArr.push(err);

	// unique
	if( Post.colParam(el,'unique') && ( err = this.colUnique(el) ) ) errArr.push(err);

	//-console.log( 'bottom of validate: errArr.length = '+errArr.length );

	// success depending on number of errors
	if( errArr.length ) {
		util.nonblockAlert( errArr.join(';\n') );
		el.focus();
		return false;
	}
	else	return true;
}

/* deprecated via html5: requried needed here since required is checked on submit, which is not necessarily done
Post.prototype.colRequired = function( el, nextFunc ) {
	console.log( "colRequired for "+el );
	nextFunc( (Post.colValue(el).length) ? null : "Entry Required" );
}
*/


Post.prototype.colUnique = function( el, nextFunc ) {

	//-console.log( "colUnique for "+el+', '+Post.pre(el) );

	//-console.log( 'unique: arg: '+JSON.stringify(this.args) );
	new Message( 'unique', {'args':this.args}, null,
		// success func
		function(message) { nextFunc() },
		// failure func
		function(message) {
			// reset message value
			el.value = (message.result.val==undefined) ? '' : message.result.val;
			el.focus();
			// return error
			console.log( 'unique error: '+message.result.message );
			nextFunc( message.result.message + "\nUse different value and try again" );
		} );
}

// return error message - blank if no error
Post.prototype.colEnum = function( el ) {

	//-console.log( "colEnum for "+el+', '+Post.pre(el) );

	var eo = JSON.stringify(el.getAttribute('enum'));

	// find enumerated value, ok
	var found = false;
	el.forEachProperty( function(att) { if( eo[att] == el.value ) { found = true; } } );

	// not found - send error 
	return (found) ? '' : "value \""+el.value+"\" was not enumerated";
}


// dlx - row only
Post.dlx = function( params, callback ) {

	var el = params.el || (params.e && params.e.target) || null;

	//-console.log( 'Post.dlx '+el );

	// find row
	params.el = el.ancestorByAttribute('tup',undefined,true);
	params.update = 'row';
	params.dml = 'delete';
	//-console.log( 'el='+el+', tup='+params.el );

	// set dml
	//-var prefix = params.el.getAttribute('tup');
	//-params.el.setAttribute(prefix+':dml','delete');

	// update based on row
	Post.update( params, callback );
}

